# 集群 cluster
30 秒速记
Node.js单进程只用一个核 →cluster起多个worker进程,共享同一个端口primary(老版本叫master)负责派生、监控、重启,worker负责处理请求- 每个
worker内存、事件循环都独立 →session、缓存、定时任务不能放进程内存里 worker崩了要自动补,但要加退避,避免启动即崩的无限重启- 现在很多团队直接用
PM2或容器副本 + 负载均衡,原理一样
cluster 解决的是「Node.js 一个进程只能用一个 CPU 核」的问题:主进程派生出多个 worker,它们共享同一个端口一起接请求。 每个 worker 都是独立进程,内存和事件循环互不相通,所以我做集群时第一件事就是把状态挪出去,session 放 Redis,定时任务只让一个进程跑,不然请求落到不同 worker 上就会出现「刚登录又掉了」。主进程监听 exit 事件补新进程,但要加退避,否则代码一启动就崩时会疯狂重启把机器拖垮。线上我更常用 PM2 的 cluster 模式,底层就是这个模块。
理解「集群 cluster」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 process 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
一个最小可跑的 cluster 例子,能直观看到「多个进程共享一个端口」:
const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isPrimary) { // Node 16 之前叫 cluster.isMaster
const n = os.availableParallelism ? os.availableParallelism() : os.cpus().length;
for (let i = 0; i < n; i++) cluster.fork();
cluster.on('exit', (w, code) => {
console.log(`worker ${w.process.pid} 退出,code=${code},补一个`);
cluster.fork(); // 生产里要加退避
});
} else {
http.createServer((req, res) => {
res.end(`handled by ${process.pid}\n`);
}).listen(3000);
}
连续 curl localhost:3000 几次,会看到返回的 pid 在变,这说明请求被分到了不同的 worker。用 kill <pid> 杀掉其中一个,主进程会收到 exit 事件并补一个新的。
几个要记住的事实:worker 是通过 child_process.fork 创建的,和主进程之间有一条 IPC 通道,可以 process.send 互发消息;默认调度策略在除 Windows 外的平台上是主进程轮询分发(SCHED_RR),可以通过 cluster.schedulingPolicy 或环境变量 NODE_CLUSTER_SCHED_POLICY 修改;每个 worker 都是完整的 V8 实例,内存各占一份,进程数一般不要超过 CPU 核数。
面试官追问
追问 1监控上看到 4 个 Node.js 进程,就能说集群可用了吗?
不能。进程活着不代表在接流量,要压一下看请求是否分摊到各个 pid,再杀掉一个 worker,看它会不会被补上、期间请求有没有大面积失败。
追问 2用户反馈刚登录完,刷新一下又要重新登录,服务刚改成 cluster,怎么回事?
session 存在进程内存里,第二次请求落到了另一个 worker。把 session 挪到 Redis,或者改用 JWT 这类无状态方案。
追问 3代码一启动就抛错,主进程不停 fork,CPU 被打满,怎么防?
exit 事件里别无脑立刻重启,要按次数做退避,比如 1s、2s、4s 递增,短时间内崩太多次就停下来并告警。
追问 4测试跑完了进程却不退出,怎么查?
看是不是 worker 没被 disconnect 或 kill,还有没有没关的 server、定时器。用 cluster.workers 检查残留,测试收尾时主动关掉,而不是让 CI 超时杀掉。
# cluster模块概览
30 秒速记
- 单个
Node.js进程只跑在一个核上 → 多核机器要起多个实例才吃得满 - 两种组织方式:多实例多端口 + 反向代理分发;或一个主进程接连接再分给子进程
Node.js的cluster是第二种:对外一个端口,主进程把连接交给worker- 业务代码几乎不用改,但进程内状态要外置(
session、缓存、定时任务)
Node.js 一个进程只用一个 CPU 核,cluster 就是让多个进程一起接请求,把多核吃满,业务代码基本不用改。 多实例的组织方式常见有两种:一种是每个实例各占一个端口,前面用 Nginx 分发;另一种是一个主进程对外监听,拿到连接后交给子进程处理,Node.js 自带的 cluster 走的是第二种。说是「几乎不用改代码」,但前提是你的代码没有把状态放在进程内存里,这一点在改造前一定要先排查。
node实例是单线程作业的。在服务端编程中,通常会创建多个node实例来处理客户端的请求,以此提升系统的吞吐率。对这样多个node实例,我们称之为cluster(集群)。
借助node的cluster模块,开发者可以在几乎不修改原有项目代码的前提下,获得集群服务带来的好处。
集群有以下两种常见的实现方案,而node自带的cluster模块,采用了方案二。
面试官追问
追问 1产品说「Node.js 单线程,多写点异步代码就等于集群了」,怎么纠正?
异步解决的是 I/O 等待时别闲着,但 JS 计算还是只在一个核上跑。CPU 吃满了要靠多进程,这才是 cluster 干的事。
追问 2改成 cluster 后吞吐几乎没涨,第一反应要不要加进程数?
先别加。确认 worker 数是不是和核数匹配、请求是否真的分散到各个 pid;再看瓶颈是不是根本不在 CPU,比如数据库或下游接口慢,加进程只会让下游更挤。
追问 3cluster 里主进程是逐字节转发请求数据给 worker 吗?
不是。主进程 accept 到连接后,把这个连接的句柄通过 IPC 传给 worker,之后数据直接在 worker 和客户端之间走,主进程不参与读写。
追问 4内存里有个计数器统计在线人数,改成 cluster 后数字对不上了,为什么?
每个 worker 各有一份计数器,各算各的。挪到 Redis 用 INCR,或者让 worker 通过 IPC 汇报给主进程统一算。
追问 5cluster 和 PM2、多容器副本,怎么选?
单机多核、想少折腾,用 PM2 -i max,它就是封装了 cluster。已经上 K8s 了就跑多个单进程副本,由平台负载均衡和重启,比在容器里再套 cluster 更好管。
# 方案一:多个node实例+多个端口
30 秒速记
- 思路:起多个独立
Node.js进程,各占一个端口(3001~3004),前面挂Nginx做反向代理分发 - 优点:简单,进程之间完全隔离,挂一个不影响别的
- 缺点:端口要管理,进程之间要交换数据只能靠外部(
Redis、消息队列) - 浏览器只认域名和一个端口,「分发」这件事一定落在代理层,健康检查也放在那
- 实际生产里
PM2/Docker多副本 +Nginxupstream基本就是这个模式
这个方案就是开几个互不相干的 Node.js 进程,各自监听一个端口,前面用 Nginx 这类反向代理把请求分过去。 好处是简单粗暴,每个进程都是普通的单进程服务,挂一个其他照常跑,代理会把它摘掉。代价是端口多了要管,进程之间没有直接的通信通道,要共享会话、缓存只能放到 Redis 这类外部存储。说白了这就是现在最常见的部署形态,容器化之后每个容器一个进程,upstream 里配多个地址,跟这个思路是一回事。
集群内的node实例,各自监听不同的端口,再由反向代理实现请求到多个端口的分发。
- 优点:实现简单,各实例相对独立,这对服务稳定性有好处。
- 缺点:增加端口占用,进程之间通信比较麻烦。
面试官追问
追问 1起了 4 个实例分别监听 3001 到 3004,浏览器会自动轮着请求吗?
不会。浏览器只访问 URL 里那个端口,根本不知道后面有几个实例。得有 Nginx 监听 80 / 443,在 upstream 里把 4 个地址配上,由它来轮询。
追问 2Nginx 那段配置大概怎么写?
upstream api { server 127.0.0.1:3001; server 127.0.0.1:3002; },再在 location 里 proxy_pass http://api;。默认就是轮询,想按负载分可以加 least_conn,同一用户要落到同一台就用 ip_hash。
